# Copyright 2009 Larry Resnik, Lasfraka-Elixir

#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.

#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.

#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.


from __future__ import division

from math import cos, sin, acos, atan, sqrt, pi
from level import (TPROP_EMPTY, TPROP_THICK, TPROP_LADDER, TPROP_NO_WALLGRIP,
    TPROP_DAMAGE, TPROP_LETHAL, COLLIDE_TYPE_NONE, COLLIDE_TYPE_CIRC,
    COLLIDE_TYPE_RECT)

if 0:
    try:
        import psyco
        psyco.profile()
        psyco.log('psycolog', 'w', 10)
    except ImportError:
        pass

# This is how long a Thing may hold off not being affected by gravity.
# This attempts to keep the game from using floating point numbers.
MAX_GRAVITY_PAUSE = 2


class Vector2D(object):
    def __init__(self, x=0.0, y=0.0):
        self.x = x
        self.y = y

    def set_direction(self, radians):
        self.x = cos(radians)
        self.y = sin(radians)

    def get_angle(self):
        return atan2(self.y, self.x)

    def normalize(self):
        magnitude = sqrt(self.x * self.x + self.y * self.y)
        try:
            self.x /= magnitude
            self.y /= magnitude
        except ZeroDivisionError:
            pass

    def __add__(self, vector):
        return Vector2D(self.x + vector.x, self.y + vector.y)

    def __sub__(self, vector):
        return Vector2D(self.x - vector.x, self.y - vector.y)

    def __repr__(self):
        return 'Vector2D: x:%f, y%f' % (self.x, self.y)


def center_anchor(graphic):
    graphic.anchor_x = int(graphic.width / 2)
    graphic.anchor_y = int(graphic.height / 2)

def to_radians(degrees):
    return pi * degrees / 180.0

def to_degrees(radians):
    return radians * 180.0 / pi

def get_distance(object1, object2):
    return sqrt((object1.x - object2.x) * (object1.x - object2.x) +
        (object1.y - object2.y) * (object1.y - object2.y))

def dot_product(vector1, vector2):
    return vector1.x * vector2.x + vector1.y * vector2.y

def point_in_rect(point, rect_left, rect_right, rect_top, rect_bottom):
    """Returns True if the point is on or within the rect boundaries."""
    return point.x > rect_left and point.x < rect_right and \
        point.y < rect_top and point.y > rect_bottom

def rect_rect_collide(rect1, rect2):
    """Returns true if the rects are touching."""
    return rect1.x + rect1.halfWidth > rect2.x - rect2.halfWidth and \
        rect1.x - rect1.halfWidth < rect2.x + rect2.halfWidth and \
        rect1.y + rect1.halfHeight > rect2.y - rect2.halfHeight and \
        rect1.y - rect1.halfHeight < rect2.y + rect2.halfHeight

def circ_circ_collide(circle1, circle2):
    """Check if this circle intersects with the passed circle.

    :rtype:  bool
    :return: True if the circles collide. False otherwise.
    """

    a = (circle1.radius + circle2.radius) * (circle1.radius + circle2.radius)
    dx = circle1.x - circle2.x
    dy = circle1.y - circle2.y
    return a > (dx * dx) + (dy * dy)

def circ_circtuple_collide(circle, circletuple):
    """Check if this circle intersects with the passed circle.

    This is a convenience method made for when the other circle must be created
    multiple times just to call the usual circle-circle collide function.
    circle is anything with an x, y, and radius members.
    circle is a tuple whose items are passed as (x, y, radius).

    :rtype:  bool
    :return: True if the circles collide. False otherwise.
    """

    a = (circle.radius + circletuple[2]) * (circle.radius + circletuple[2])
    dx = circle.x - circletuple[0]
    dy = circle.y - circletuple[1]
    return a > (dx * dx) + (dy * dy)

def circ_rect_collide(circle, rect):
    """Returns True if the circle and rect have collided."""
    # Get the center of the sphere relative to the center of the box
    rectCenter = Vector2D(rect.x + rect.width / 2, rect.y + rect.height / 2)
    circleCenterRelativeRect = Vector2D(circle.x, circle.y) - rectCenter
    # Point on surface of box that is closest to the center of the sphere
    rectPoint = Vector2D()

    # Check sphere center against box along the X axis alone.
    # If the sphere is off past the left edge of the box,
    # then the left edge is closest to the sphere.
    # Similar if it's past the right edge. If it's between
    # the left and right edges, then the sphere's own X
    # is closest, because that makes the X distance 0,
    # and you can't get much closer than that :)
    if circleCenterRelativeRect.x < -rect.width / 2.0:
        rectPoint.x = -rect.width / 2.0
    elif circleCenterRelativeRect.x > rect.width / 2.0:
        rectPoint.x = rect.width / 2.0
    else:
        rectPoint.x = circleCenterRelativeRect.x

    # ...same for Y axis
    if circleCenterRelativeRect.y < -rect.height / 2.0:
        rectPoint.y = -rect.height / 2.0
    elif circleCenterRelativeRect.y > rect.height / 2.0:
        rectPoint.y = rect.height / 2.0
    else:
        rectPoint.y = circleCenterRelativeRect.y

    # Now we have the closest point on the box, so get the distance from
    # that to the sphere center, and see if it's less than the radius
    dist = circleCenterRelativeRect - rectPoint;

    return dist.x * dist.x + dist.y * dist.y < circle.radius * circle.radius

def calculateBorders(x, y, obj):
    """Finds the array indices of the tiles around obj.

    The indices are based on obj's center and its halfHeight and halfWidth.
    The values are calculated as floating point numbers so that the case that
    any of the values are negative yet are greater than -1.

    :rtype: A tuple of 4 floats.
    :return: A tuple whose values correspond to (top, bottom, left, right).
    """

    top = (y + obj.halfHeight - 1) / 32
    bottom = (y - obj.halfHeight) / 32
    left = (x - obj.halfWidth) / 32
    right = (x + obj.halfWidth - 1) / 32
    return (top, bottom, left, right)

def moveVertical(obj, directionY, level):
    """Try to move obj based on directionY.

    The function user is expected to assign obj.ySpeed manually.

    :rtype: Tile type (int)
    :return: The most significant (highest valued) tile that was contacted.
        TPROP_EMPTY (0) is returned if obj's ySpeed was 0 before calculating
        collision.
    """

    top, bottom, left, right = calculateBorders(obj.x, obj.y + obj.ySpeed, obj)
    if directionY > 0:
        topLeft = level.get_at(top, left)
        topRight = level.get_at(top, right)
        if max(topLeft, topRight) < TPROP_THICK:
            obj.y += obj.ySpeed
        else:
            obj.y = int(int(top) * 32 - obj.height / 2)
        return max(topLeft, topRight)
    elif directionY < 0:
        bottomLeft = level.get_at(bottom, left)
        bottomRight = level.get_at(bottom, right)
        if max(bottomLeft, bottomRight) < TPROP_THICK:
            obj.y += obj.ySpeed
        else:
            obj.y = int(int(bottom + 1) * 32 + obj.height / 2)
        return max(bottomLeft, bottomRight)
    return TPROP_EMPTY

def move_horizontal(obj, direction, speed, level):
    """Try to move obj based on direction.

    speed is expected to be a positive number.
    direction is expected to be positive for right and negative for left.
    level is the collidable grid.

    :rtype: Tile type (int)
    :return: The most significant (highest valued) tile that was contacted.
        TPROP_EMPTY (0) is returned if obj's ySpeed was 0 before calculating
        collision.
    """

    significantTile = TPROP_EMPTY
    if direction < 0:
        speed *= -1
    top, bottom, left, right = calculateBorders(obj.x + speed, obj.y, obj)
    if direction < 0:
        significantTile = max(level.get_at(top, left),
            level.get_at(bottom, left))
        if significantTile < TPROP_THICK:
            obj.x += speed
        else:
            obj.x = int(int(left + 1) * 32 + obj.width / 2)
    elif direction > 0:
        significantTile = max(level.get_at(top, right),
            level.get_at(bottom, right))
        if significantTile < TPROP_THICK:
            obj.x += speed
        else:
            obj.x = int(int(right) * 32 - obj.width / 2)
    return significantTile

def move_vertical(obj, direction, speed, level):
    """Try to move obj based on directionY.

    speed is expected to be a positive number.
    direction is expected to be positive for right and negative for left.
    level is the collidable grid.

    :rtype: Tile type (int)
    :return: The most significant (highest valued) tile that was contacted.
        TPROP_EMPTY (0) is returned if obj's ySpeed was 0 before calculating
        collision.
    """

    significantTile = TPROP_EMPTY
    if direction < 0:
        speed *= -1
    top, bottom, left, right = calculateBorders(obj.x, obj.y + speed, obj)
    if direction > 0:
        significantTile = max(level.get_at(top, left),
            level.get_at(top, right))
        if significantTile < TPROP_THICK:
            obj.y += speed
        else:
            obj.y = int(int(top) * 32 - obj.height / 2)
    elif direction < 0:
        significantTile = max(level.get_at(bottom, left),
            level.get_at(bottom, right))
        if significantTile < TPROP_THICK:
            obj.y += speed
        else:
            obj.y = int(int(bottom + 1) * 32 + obj.height / 2)
    return significantTile

def jump(obj, level):
    """obj accelerates due to gravity and then moves up or down.

    obj's ySpeed is set to 0 if any of the contacted tiles were thick. This
    could mean a ceiling or a floor.

    :rtype: Tile type (int)
    :return: The most significant (highest valued) tile that was contacted.
        TPROP_EMPTY (0) is returned if obj's ySpeed was 0 before calculating
        collision.
    """

    obj.gravityPause += 1
    if obj.gravityPause >= MAX_GRAVITY_PAUSE:
        obj.gravityPause = 0
        obj.ySpeed -= 1
    if obj.ySpeed < -8:
        obj.ySpeed = -8
    if obj.ySpeed > 0:
        return_val = moveVertical(obj, 1, level)
        if return_val >= TPROP_THICK:
            # We hit something from above.
            obj.ySpeed = 0
        return return_val
    elif obj.ySpeed < 0:
        return_val = moveVertical(obj, -1, level)
        if return_val >= TPROP_THICK:
            # We landed on something.
            obj.ySpeed = 0
            obj.gravityPause = 0
        return return_val
    return TPROP_EMPTY

def jump_beta(obj, level):
    """obj accelerates due to gravity and then moves up or down.

    obj's ySpeed is set to 0 if any of the contacted tiles were thick. This
    could mean a ceiling or a floor.

    :rtype: Tile type (int)
    :return: The most significant (highest valued) tile that was contacted.
        TPROP_EMPTY (0) is returned if obj's ySpeed was 0 before calculating
        collision.
    """

    obj.ySpeed -= 1
    if obj.ySpeed < -8:
        obj.ySpeed = -8
    significantTile = TPROP_EMPTY
    if obj.ySpeed > 0:
#        return_val = moveVertical(obj, 1, level)
        significantTile = move_vertical(obj, 1, obj.ySpeed, level)
        if significantTile >= TPROP_THICK:
            # We hit something from above.
            obj.ySpeed = 0
    elif obj.ySpeed < 0:
#        return_val = moveVertical(obj, -1, level)
        significantTile = move_vertical(obj, -1, obj.ySpeed, level)
        if significantTile >= TPROP_THICK:
            # We landed on something.
            obj.ySpeed = 0
    return significantTile

def fall(obj, level):
    """Attempt to fall down.

    Checks the area beneath obj for collidability. If all tiles below obj are
    not collidable, obj's ySpeed is reset to 0. Otherwise, nothing is done.

    :rtype: Tile type (int)
    :return: The most significant (highest valued) tile that was contacted.
    """

    top, bottom, left, right = calculateBorders(obj.x, obj.y - 1, obj)
    bottomLeft = level.get_at(bottom, left)
    bottomRight = level.get_at(bottom, right)
    if max(bottomLeft, bottomRight) < TPROP_THICK:
        # Everything below our feet is air. Start to fall.
        obj.ySpeed = 0
    return max(bottomLeft, bottomRight)

def checkUpLadder(obj, tiles, debugText = None):
    """Attempt to move obj upwards on a ladder.

    * If the tile above obj is hollow, obj will stand on the blank tile.
    * If the tile above obj is thick, obj will stop under the thick tile.
    * Otherwise, obj moves upwards by obj.ladderSpeed.

    :return: True if obj is on a ladder. False otherwise.
    """

    calculateLadderBorders(obj.x, obj.y - obj.ladderSpeed, obj)
    downY = (obj.y + obj.height - 1) / 32
    upY = (obj.y - obj.height - obj.ladderSpeed) / 32
    topTile = tiles[obj.top][obj.left]
    upLadder = topTile.ladder
    upThick = topTile.thick
    downLadder = tiles[obj.bottom][obj.left].ladder
    # Check if the tile above the obj is thick. If this is not done, the
    # obj will jitter when there is a thick tile above the ladder.
    calculateBorders(obj.x, obj.y - obj.ladderSpeed, obj)
    upThick |= tiles[obj.top][obj.left].thick
    if debugText:
        debugText.setText("downY: %i upY: %i upLadder: %i downLadder: %i" % (
            downY, upY, upLadder, downLadder) )
    if upThick:
        # The tile above the ladder is thick. Stop at the top of the ladder.
        obj.y = (upY + 1) * 32 + obj.height
        return False
    elif upLadder or downLadder:
        return True
    # Stand on top of the ladder.
    if obj.state is not obj.LADDER:
        return False
    obj.state = obj.STAND
    # This part confuses the crap out of me, but it seems to work anyways.
    #difference = (downY - upY) is 2
    obj.y = (upY + 1 - ( (downY - upY) == 2) ) * 32 + obj.height
    return False

def checkDownLadder(obj, tiles, debugText = None):
    """Attempt to move the obj downwards on a ladder.

    * If the tile below obj is hollow, obj will fall using fall().
    * If the tile below obj is thick, obj will stand at that spot.
    * Otherwise, obj moves downwards by obj.ladderSpeed.

    :return: True if obj is on a ladder. False otherwise.
    """

    calculateBorders(obj.x, obj.y + obj.ladderSpeed, obj)
    downY = (obj.y + obj.ladderSpeed + obj.height) / 32
    downTile = tiles[obj.bottom][obj.left]
    downThick = downTile.thick
    downLadder = downTile.ladder
    if debugText:
        debugText.setText("downY: %i downLadder: %i" % (downY, downLadder) )
    if obj.state is obj.LADDER and downThick:
        # The tile below the ladder is thick. Stand on the ground.
        obj.y = (downY - 2) * 32 + obj.height
        obj.state = obj.STAND
        return False
    elif downLadder:
        return True
    # The tile below the ladder is hollow. Start falling.
    fall(obj, tiles)
    return False

def getOnLadder(obj, tiles, debugText = None):
    """It checks the nearby tiles for a ladder. If there is a ladder, obj is
    centered on the ladder tile's x-axis.

    * If a thick tile is above the ladder tile the player is on, the player
    stops before that tile.
    * If an empty tile is above the ladder tile the player is on, the player
    will stand on top of the ladder tile.

    :return: True is obj grabbed onto a ladder. False otherwise.
    """

    # Check the borders based on the ladder collision specifications: a
    # quarter of the radius outside of the player's center.
    calculateLadderBorders(obj.x, obj.y - obj.ladderSpeed, obj)
    # Check obj's left side.
    topTile = tiles[obj.top][obj.left]
    downLadderLeft = topTile.ladder
    upLadderLeft = topTile.ladder
    # Check obj's right side.
    topTile = tiles[obj.top][obj.right]
    downLadderRight = topTile.ladder
    upLadderRight = topTile.ladder

    # Check the tiles around the player's actual size.
    calculateBorders(obj.x, obj.y - obj.ladderSpeed, obj)
    # See if the tile above is thick.

    #   Note: The thickness variables can't be checked solo in the separate
    # (downLadder or upLadder) statements or else a glitch comes up where
    # trying to climb on a blank tile between two ladder tiles makes you
    # suddenly hope above the blank spot.
    upThickLeft = tiles[obj.top][obj.left].thick
    upThickRight = tiles[obj.top][obj.right].thick

    # Check if the tiles in the player's borders are really ladders just like
    # the ones checked by the ladder-borders.

    #   Note: Removing this does nothing apparent but will introduces an
    # obscure glitch where jumping off of a ladder with a thick tile above it
    # to the left side where a blank tile resides causes you to grab the ladder
    # on the blank tile. This is because of the difference between the ladder
    # collision borders and the player's actual collision borders. Also, it is
    # affected by the order of which ladder side is examined (left is checked
    # first, so the glitch happens on the left).
    upLadderLeft &= tiles[obj.top][obj.left].ladder
    downLadderLeft &= tiles[obj.bottom][obj.left].ladder

    if debugText:
        debugText.setText("top:%i bot:%i lft:%i rgt:%i " \
            "upLft:%i upRgt:%i dnLft:%i dnRgt:%i thickL:%i thickR:%i" % (
            obj.top, obj.bottom, obj.left, obj.right,
            upLadderLeft, upLadderRight, downLadderLeft, downLadderRight,
            upThickLeft, upThickRight) )

    if upThickLeft or upThickRight:
        # The tile above the ladder is thick. Stop at the top of the ladder.
        #obj.y = (upY + 1) * 32 + obj.height
        return False
    # The tile above is not thick. Start climbing.
    elif downLadderLeft or upLadderLeft:
        obj.x = obj.left * 32
        return True
    elif downLadderRight or upLadderRight:
        obj.x = obj.right * 32
        return True
    # There is no more ladder to climb up.
    if obj.state is not obj.LADDER:
        # There wasn't a ladder to climb up in the first place.
        return False
    # We've reached the top of a ladder with a blank tile on top. Stand on it.
    obj.state = obj.STAND
    downY = (obj.y + obj.quarterHeight - 1) / 32
    upY = (obj.y - obj.quarterHeight - obj.ladderSpeed) / 32
    # This part confuses the crap out of me, but it seems to work anyways.
    #difference = (downY - upY) is 2
    obj.y = (upY + 1 - ( (downY - upY) == 2) ) * 32 + 8
    return False

def getOnLadderBelow(obj, tiles, debugText = None):
    """It checks the tile(s) below obj for a ladder.

    * If there isn't a ladder, False is returned and nothing is done.

    :return: True if obj is on a ladder. False otherwise.
    """

    # Check the borders.
    calculateBorders(obj.x, obj.y + obj.ladderSpeed, obj)
    downLadderLeft = tiles[obj.bottom][obj.left].ladder
    downLadderRight = tiles[obj.bottom][obj.right].ladder

    if debugText:
        debugText.setText("dnLft:%i dnRgt:%i"%(downLadderLeft, downLadderRight))

    if downLadderLeft:
        obj.x = obj.left * 32
        return True
    elif downLadderRight:
        obj.x = obj.right * 32
        return True
    # There is no more ladder to climb.
    if obj.state is not obj.LADDER:
        # There wasn't a ladder to climb up in the first place.
        return False
    # We've reached the bottom of the ladder. Stand.
    obj.state = obj.STAND
    downY = (obj.y + obj.quarterHeight - 1) / 32
    upY = (obj.y - obj.quarterHeight - obj.ladderSpeed) / 32
    # This part confuses the crap out of me, but it seems to work anyways.
    #difference = (downY - upY) is 2
    obj.y = ( upY + 1 - ( (downY - upY) == 2 ) ) * 32 + 8
    return False

def climbLadder(obj, directionY):
    """Sets obj.state to LADDER and moves obj by its ladderSpeed.

    No collision detection is involved.
    """
    obj.state = obj.LADDER
    obj.y += obj.ladderSpeed * directionY
    return
